iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0

這篇文章將說明在 APS 架構下,利用 SharedPreferences 取代傳統的暫存資料結構(如 ArrayList 或 HashMap),
達成「資料永久保存」的功能,確保 App 關閉後資料仍存在。
只需將上一篇教學裡的LoginData換成SharedPrefsManager就可以了

專案結構說明

整個專案分為三個主要部分:

  1. MainActivity:負責登入與註冊。
  2. HomeActivity:顯示使用者資料。
  3. SharedPrefsManager:負責資料的永久儲存與存取。

這樣的分層能符合 APS 架構原則,使資料處理與畫面顯示分離。

MainActivity(登入與註冊)

此 Activity 提供三個輸入欄位:

  • 使用者名稱(Username)
  • 電子郵件(Email)
  • 密碼(Password)

並具備兩個主要功能:

  1. 註冊功能

    • 驗證 Email 格式
    • 驗證密碼是否為 6 碼以上的數字
    • 若帳號或 Email 已存在則提示錯誤
    • 新帳號會以 JSON 格式儲存至 SharedPreferences
  2. 登入功能

    • 使用者輸入三項資料
    • 若驗證成功則導向 HomeActivity
    • 登入失敗則提示錯誤訊息

同時預設建立一組管理員帳號(admin@gmail.com / 123456)方便測試。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);

        bindUI();
    }

    protected void bindUI() {
        saveButton = findViewById(R.id.main_save_btn);
        loginButton = findViewById(R.id.main_login_btn);
        emailEditText = findViewById(R.id.main_email_et);
        usernameEditText = findViewById(R.id.main_username_et);
        passwordEditText = findViewById(R.id.main_password_et);
        prefsManager = new SharedPrefsManager(this);

        // 如果資料庫為空則預設加入 admin 帳號
        if (prefsManager.getUserList().length() == 0) {
            JSONObject admin = new JSONObject();
            try {
                admin.put("username", "admin");
                admin.put("email", "admin@gmail.com");
                admin.put("password", "123456");
                prefsManager.saveUser(admin);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        TextWatcher watcher = new TextWatcher() {
            @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
            @Override public void onTextChanged(CharSequence s, int start, int before, int count) { check(); }
            @Override public void afterTextChanged(Editable s) {}
        };

        emailEditText.addTextChangedListener(watcher);
        usernameEditText.addTextChangedListener(watcher);
        passwordEditText.addTextChangedListener(watcher);

        check();

        saveButton.setOnClickListener(this::onSaveButtonClick);
        loginButton.setOnClickListener(this::onLoginButtonClick);
    }

    private void clear() {
        emailEditText.setText("");
        usernameEditText.setText("");
        passwordEditText.setText("");
    }

    private void check() {
        String username = usernameEditText.getText().toString();
        String email = emailEditText.getText().toString();
        String password = passwordEditText.getText().toString();

        boolean isAllFilled = !email.isEmpty() && !password.isEmpty() && !username.isEmpty();
        boolean isEmail = Patterns.EMAIL_ADDRESS.matcher(email).matches();
        boolean isPassword = Pattern.matches("\\d{6,}", password);

        emailEditText.setOnFocusChangeListener((v, hasFocus) -> {
            if (!hasFocus) {
                if (!isEmail && !email.isEmpty()) {
                    emailEditText.setError("電子郵件格式錯誤");
                } else {
                    emailEditText.setError(null);
                }
            }
        });

        passwordEditText.setOnFocusChangeListener((v, hasFocus) -> {
            if (!hasFocus) {
                if (!isPassword && !password.isEmpty()) {
                    passwordEditText.setError("密碼需至少6碼數字");
                } else {
                    passwordEditText.setError(null);
                }
            }
        });

        saveButton.setEnabled(isAllFilled && isEmail && isPassword);
        loginButton.setEnabled(isAllFilled && isEmail && isPassword);
    }

    private void onSaveButtonClick(View view) {
        String username = usernameEditText.getText().toString();
        String email = emailEditText.getText().toString();
        String password = passwordEditText.getText().toString();

        //呼叫在SharedPrefsManager已經寫好的prefsManager.userExists()檢查是否存在相同的用戶名稱或email
        if (prefsManager.userExists(username, email)) {
            Toast.makeText(this, "用戶或 Email 已存在", Toast.LENGTH_SHORT).show();
        } else {
            JSONObject newUser = new JSONObject();
            try {
                newUser.put("username", username);
                newUser.put("email", email);
                newUser.put("password", password);
                prefsManager.saveUser(newUser);
                Toast.makeText(this, "已儲存", Toast.LENGTH_SHORT).show();
                clear();
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

    private void onLoginButtonClick(View view) {
        String username = usernameEditText.getText().toString();
        String email = emailEditText.getText().toString();
        String password = passwordEditText.getText().toString();

        check();

        if (prefsManager.isLoginValid(username, email, password)) {
            Intent intent = new Intent(this, HomeActivity.class);
            intent.putExtra("username", username);
            startActivity(intent);
            Toast.makeText(this, "Hello, " + username, Toast.LENGTH_SHORT).show();
            clear();
        } else {
            Toast.makeText(this, "登入失敗", Toast.LENGTH_SHORT).show();
        }
    }

HomeActivity(資料顯示)

登入成功後會跳轉到 HomeActivity。
此畫面透過 SharedPrefsManager 讀取資料,並根據使用者角色顯示不同內容:

  • 若為 admin:顯示所有註冊過的帳號資料(完整清單)。
  • 若為一般使用者:只顯示自己的帳號資料(帳號、Email、密碼)。

這樣的設計可以模擬簡單的權限系統,讓管理員與一般使用者看到的資訊有所區別。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_home);

        bindUI();
    }

    protected void bindUI() {
        getdataButton = findViewById(R.id.home_getdata_btn);
        backButton = findViewById(R.id.home_back_btn);
        showdataTextView = findViewById(R.id.home_showdata_tv);
        backButton.setOnClickListener(v -> finish());

        //將點擊事件獨立出去
        getdataButton.setOnClickListener(this::onGetDataButtonClick);
    }

    protected void onGetDataButtonClick(View view) {
        Intent intent = getIntent();
        String username = intent.getStringExtra("username");

        SharedPrefsManager prefsManager = new SharedPrefsManager(this);
        //取得所有使用者的資料
        JSONArray users = prefsManager.getUserList();
        StringBuilder sb = new StringBuilder();

        //如果使用者名稱為admin則顯示所有使用者的資料
        if ("admin".equals(username)) {
            // 顯示所有使用者
            for (int i = 0; i < users.length(); i++) {
                //從user中取出第 i 筆資料,並轉成 JSONObject 物件。
                //JSONObject:用來表示一組鍵值資料(Key-Value)的類別
                JSONObject user = users.optJSONObject(i);
                if (user != null) {
                    sb.append(String.format("帳號%d:\n用戶名:%s\n電子郵件:%s\n密碼:%s\n\n",
                            i + 1,
                            //取出這個使用者的「用戶名」欄位(key 為 "username")
                            user.optString("username"),
                            user.optString("email"),
                            user.optString("password")));
                }
            }
        } else {
            // 顯示自己
            boolean found = false;
            for (int i = 0; i < users.length(); i++) {
                JSONObject user = users.optJSONObject(i);
                //如果user != null且和尋找到的這筆資料的username相同,則顯示此筆資料
                if (user != null && username.equals(user.optString("username"))) {
                    sb.append(String.format("用戶名:%s\n電子郵件:%s\n密碼:%s\n",
                            user.optString("username"),
                            user.optString("email"),
                            user.optString("password")));
                    //表示找到了,停止迴圈
                    found = true;
                    break;
                }
            }
            //沒找到(found = false)
            if (!found) sb.append("找不到使用者資料");
        }
        //設置顯示的資料
        showdataTextView.setText(sb.toString());
    }

SharedPrefsManager(資料儲存層)

SharedPrefsManager 是專門管理 SharedPreferences 操作的類別。
它提供四項核心功能:

  1. 取得所有使用者資料:以 JSON 陣列形式回傳。
  2. 儲存新使用者:將註冊資料新增進 SharedPreferences。
  3. 檢查重複帳號:避免重複註冊相同使用者或 Email。
  4. 驗證登入:比對帳號、Email、密碼是否符合。

使用 JSON 儲存多筆資料的方式簡潔且直觀,適合簡易登入系統。

public class SharedPrefsManager {
    private static final String PREF_NAME = "UserPrefs"; //在 Android 裡建立 SharedPreferences 時指定的檔案名稱
    private static final String KEY_USERS = "users"; //存放「所有用戶資料」這筆資料在 SharedPreferences 裡的 key(鍵名)
    private SharedPreferences prefs; //用來操作 SharedPreferences 的物件

    // 建構子,傳入 Context 取得 SharedPreferences
    public SharedPrefsManager(Context context) {
        // Android 系統取得名為 PREF_NAME的 SharedPreferences 物件
        //Context.MODE_PRIVATE:表示這份 SharedPreferences 只能被本 App 存取,不會跟其他 App 共享。
        prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
    }

    // getUserList()被呼叫則取得所有用戶資料(JSONArray 格式)
    public JSONArray getUserList() {
        String json = prefs.getString(KEY_USERS, "[]"); // 沒資料預設回傳空陣列
        try {
            return new JSONArray(json); // 成功時把字串 json 解析成 JSONArray並回傳
        } catch (JSONException e) {
            return new JSONArray(); // 格式錯誤時回傳空陣列
        }
    }

    // 儲存一個新用戶(加入陣列並覆蓋原本資料)
    public void saveUser(JSONObject user) {
        JSONArray users = getUserList(); // 先取得現有所有用戶
        users.put(user); // 新用戶加進陣列
        prefs.edit().putString(KEY_USERS, users.toString()).apply(); // 轉字串存回 SharedPreferences
    }

    // 檢查是否已有相同 username 或 email 的用戶
    public boolean userExists(String username, String email) {
        //取得所有用戶資料
        JSONArray users = getUserList();
        //迴圈從第一個開始檢查
        for (int i = 0; i < users.length(); i++) {
            JSONObject u = users.optJSONObject(i);
            // 若 username 或 email 有重複就回傳 true
            if (u != null && (username.equals(u.optString("username")) || email.equals(u.optString("email")))) {
                return true;
            }
        }
        return false; // 沒有重複回傳 false
    }

    // 檢查使用者登入資料是否正確(需 username、email、password 全部符合才算成功)
    public boolean isLoginValid(String username, String email, String password) {
        JSONArray users = getUserList();
        for (int i = 0; i < users.length(); i++) {
            JSONObject u = users.optJSONObject(i);
            if (u != null &&
                    username.equals(u.optString("username")) &&
                    email.equals(u.optString("email")) &&
                    password.equals(u.optString("password"))) {
                return true; // 找到完全符合的用戶回傳 true
            }
        }
        return false; // 沒有找到符合的回傳 false
    }

    // 取得所有用戶資料(和 getUserList 一樣)
    public JSONArray getAllUsers() {
        return getUserList();
    }

    // 依照 username 取得單一用戶的資料(找不到就回傳 null)
    public JSONObject getUser(String username) {
        JSONArray users = getUserList();
        for (int i = 0; i < users.length(); i++) {
            JSONObject u = users.optJSONObject(i);
            if (username.equals(u.optString("username"))) return u;
        }
        return null;
    }

    // 清空 SharedPreferences 所有資料
    public void clear() {
        prefs.edit().clear().apply();
    }
}

上一篇
Day 25.APS架構(假資料)
下一篇
Day 27. SharedPrefsManager 與 LoginData 比較與對比
系列文
Android 新手的 30 天進化論:從初學者到小專案開發者30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言